/*
 * Copyright (C)  Tony Green, Litepal Framework Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.litepal.tablemanager;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.litepal.tablemanager.model.TableModel;
import org.litepal.util.BaseUtility;
import org.litepal.util.LogUtil;

import android.database.sqlite.SQLiteDatabase;

/**
 * Upgrade the database. The first step is to remove the columns that can not
 * find the corresponding field in the model class. Then add the new added field
 * as new column into the table. At last it will check all the types of columns
 * to see which are changed.
 *
 * @author Tony Green
 * @since 1.0
 */
public class Upgrader extends AssociationUpdater {
    /**
     * Model class for table.
     */
    protected TableModel mTableModel;

    /**
     * Analyzing the table model, them remove the dump columns and add new
     * columns of a table.
     */
    @Override
    protected void createOrUpgradeTable(SQLiteDatabase db, boolean force) {
        mDb = db;
        for (TableModel tableModel : getAllTableModels()) {
            mTableModel = tableModel;
            upgradeTable();
        }
    }

    /**
     * Upgrade table actions. Include remove dump columns, add new columns and
     * change column types. All the actions above will be done by the
     * description order.
     */
    private void upgradeTable() {
        removeColumns(findColumnsToRemove(), mTableModel.getTableName());
        addColumn(findColumnsToAdd());
        changeColumnsType(findColumnTypesToChange());
    }

    /**
     * It will find the difference between class model and table model. If
     * there's a field in the class without a corresponding column in the table,
     * this field is a new added column. This method find all new added columns.
     *
     * @return A map contains all new columns with column name as key and column
     * type as value.
     */
    private Map<String, String> findColumnsToAdd() {
        Map<String, String> newColumnsMap = new HashMap<String, String>();
        for (String columnName : mTableModel.getColumnNames()) {
            boolean isNewColumn = true;
            for (String dbColumnName : getTableModelFromDB(mTableModel.getTableName())
                    .getColumnNames()) {
                if (columnName.equalsIgnoreCase(dbColumnName)) {
                    isNewColumn = false;
                    break;
                }
            }
            if (isNewColumn) {
                // add column action
                if (!isIdColumn(columnName)) {
                    newColumnsMap.put(columnName, mTableModel.getColumns().get(columnName));
                }
            }
        }
        return newColumnsMap;
    }

    /**
     * This method helps find the difference between table model from class and
     * table model from database. Database should always be synchronized with
     * model class. If there're some fields are removed from class, the table
     * model from database will be compared to find out which fields are
     * removed. But there're still some exceptions. The columns named id or _id
     * won't ever be removed. The foreign key column will be checked some where
     * else, not from here.
     *
     * @return A list with column names need to remove.
     */
    private List<String> findColumnsToRemove() {
        TableModel tableModelDB = getTableModelFromDB(mTableModel.getTableName());
        List<String> removeColumns = new ArrayList<String>();
        Map<String, String> dbColumnsMap = tableModelDB.getColumns();
        Set<String> dbColumnNames = dbColumnsMap.keySet();
        for (String dbColumnName : dbColumnNames) {
            if (isNeedToRemove(dbColumnName)) {
                removeColumns.add(dbColumnName);
            }
        }
        for (String removeColumn : removeColumns) {
            LogUtil.d(TAG, "remove column is >> " + removeColumn);
        }
        return removeColumns;
    }

    /**
     * It will check each class in the mapping list. Find their types for each
     * field is changed or not by comparing with the types in table columns. If
     * there's a column have same name as a field in class but with different
     * type, then it's a type changed column.
     *
     * @return A map contains all type changed columns with column name as key
     * and column type as value.
     */
    private Map<String, String> findColumnTypesToChange() {
        Map<String, String> changeTypeColumns = new HashMap<String, String>();
        TableModel tableModelDB = getTableModelFromDB(mTableModel.getTableName());
        for (String columnNameDB : tableModelDB.getColumnNames()) {
            for (String columnName : mTableModel.getColumnNames()) {
                if (columnNameDB.equalsIgnoreCase(columnName)) {
                    String columnTypeDB = tableModelDB.getColumns().get(columnNameDB);
                    String columnType = mTableModel.getColumns().get(columnName);
                    if (!columnTypeDB.equalsIgnoreCase(columnType)) {
                        // column type is changed
                        changeTypeColumns.put(columnName, columnType);
                    }
                }
            }
        }
        return changeTypeColumns;
    }

    /**
     * Tell LitePal the column is need to remove or not. The column can be
     * remove only on the condition that the following three rules are all
     * passed. First the corresponding field for this column is removed in the
     * class. Second this column is not an id column. Third this column is not a
     * foreign key column.
     *
     * @param columnName The column name to judge
     * @return Need to remove return true, otherwise return false.
     */
    private boolean isNeedToRemove(String columnName) {
        return isRemovedFromClass(columnName) && !isIdColumn(columnName)
                && !isForeignKeyColumn(mTableModel, columnName);
    }

    /**
     * Read a column name from database, and judge the corresponding field in
     * class is removed or not.
     *
     * @param columnName The column name to judge.
     * @return If it's removed return true, or return false.
     */
    private boolean isRemovedFromClass(String columnName) {
        return !BaseUtility.containsIgnoreCases(mTableModel.getColumnNames(), columnName);
    }

    /**
     * Generate a SQL for add new column into the existing table.
     *
     * @param columnName The new column name.
     * @param columnType The new column type.
     * @return A SQL to add new column.
     */
    private String generateAddColumnSQL(String columnName, String columnType) {
        return generateAddColumnSQL(mTableModel.getTableName(), columnName, columnType);
    }

    /**
     * This method create a SQL array for the all new columns to add them into
     * table.
     *
     * @param newColumnsMap A column map with column name as key and column type as value.
     * @return A SQL array contains add all new columns job.
     */
    private String[] getAddColumnSQLs(Map<String, String> newColumnsMap) {
        List<String> sqls = new ArrayList<String>();
        for (String columnName : newColumnsMap.keySet()) {
            sqls.add(generateAddColumnSQL(columnName, newColumnsMap.get(columnName)));
        }
        return sqls.toArray(new String[0]);
    }

    /**
     * When some fields are added into the class after last upgrade, the table
     * should synchronize the changes by adding the corresponding columns.
     *
     * @param columnsMap A map contains all the new columns need to add with column
     *                   names as key and column types as value.
     */
    private void addColumn(Map<String, String> columnsMap) {
        execute(getAddColumnSQLs(columnsMap), mDb);
    }

    /**
     * When some fields type are changed in class, the table should drop the
     * before columns and create new columns with same name but new types.
     *
     * @param changeTypeColumns A map contains all the columns need to change type with column
     *                          names as key and column types as value.
     */
    private void changeColumnsType(Map<String, String> changeTypeColumns) {
        removeColumns(changeTypeColumns.keySet(), mTableModel.getTableName());
        addColumn(changeTypeColumns);
    }

}
